#region MIT Licence
/*
 * Copyright (C) 2014 PdfCreative
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
 * and associated documentation files (the "Software"), to deal in the Software without restriction, 
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, 
 * sublicense, and/or sell copies of the Software, and to permit persons to whom 
 * the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included 
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 
 * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#endregion

using System;
using System.Collections.Generic;
using System.Text;

using PdfCreative.Pdf.Writer.Utilities;

namespace PdfCreative.Pdf.Writer.Embedding
{
    /// <summary>
    /// The PdfObject class represents an object definition in a PDF stream. It is used by the PdfParser to build up a PDF object as it is
    /// read from a source stream. The object is written back to the output stream after having been fully parsed.
    /// </summary>
    internal class PdfObject
    {
        // Private fields
        // ==============
        #region Private fields
        private PdfObjectReference _identifier;             // The object's identifier.
        private PdfDictionary _dictionary;                  // The dictionary of the object.
        private PdfValue _value;                            // The object's value.

        private static byte[] _endStream = System.Text.ASCIIEncoding.ASCII.GetBytes("endstream");
                                                            // The byte representation of an 'endstream'
                                                            // token.
        #endregion



        // Public events
        // =============
        #region Public events
        /// <summary>
        /// The Stream event is raised when the PdfObject instance encounters a stream while parsing the
        /// object data. Listeners can handle the stream parsing from this moment, or decide not to, in
        /// which case the PdfObject instance will handle the stream.
        /// </summary>
        public event StreamParseEventHandler Stream;
        #endregion



        // Public properties
        // =================
        #region Public properties
        /// <summary>
        /// Sets or retrieves the object's identifier.
        /// </summary>
        public PdfObjectReference Identifier
        {
            get { return _identifier; }
            set { _identifier = value; }
        }

        /// <summary>
        /// Sets or retrieves the dictionary of this object.
        /// </summary>
        public PdfDictionary Dictionary
        {
            get { return _dictionary; }
            set { _dictionary = value; }
        }

        /// <summary>
        /// Retrieves the object's type.
        /// </summary>
        public string Type
        {
            get
            {
                return _dictionary["/Type"];
            }
        }

        /// <summary>
        /// Sets or retrieves the object's value. This is initially equal to the object's
        /// dictionary, and usually remains that way. It can be set to another PdfValue
        /// instance.
        /// </summary>
        public PdfValue Value
        {
            get { return _value; }
            set { _value = value; }
        }
        #endregion

        

        // Public methods
        // ==============
        #region Public methods
        /// <summary>
        /// Parses the PDF object at the lexer's current position.
        /// </summary>
        /// <param name="lexer">The lexer to use</param>
        public void Parse(PdfLexer lexer)
        {
            Parse(lexer, false);
        }

        /// <summary>
        /// Parses the PDF object at the lexer's current position.
        /// </summary>
        /// <param name="lexer">The lexer to use</param>
        /// <param name="ignoreObjectHeader">Should the parser skip looking for the 'obj' header token?</param>
        public void Parse(PdfLexer lexer, bool ignoreObjectHeader)
        {
            if (null == lexer) throw new ArgumentNullException("lexer");

            InternalParse(lexer, ignoreObjectHeader);
        }

        /// <summary>
        /// Sets the value of the object.
        /// </summary>
        /// <param name="value">The value</param>
        public void SetValue(string value)
        {
            _value = new PdfPrimitive(value);
        }
        #endregion



        // Protected implementation
        // ========================
        #region Protected implementation
        /// <summary>
        /// Raises the Stream event.
        /// </summary>
        /// <returns>A value indicating if the event listener has handled the stream content or not.</returns>
        protected virtual bool OnStream()
        {
            bool isHandled = false;

            if (null != Stream)
                isHandled = Stream();

            return isHandled;
        }
        #endregion



        // Private implementation
        // ======================
        #region Private implementation
        /// <summary>
        /// Parses the PDF object at the lexer's current position.
        /// </summary>
        /// <param name="lexer">The lexer</param>
        /// <param name="ignoreObjectHeader">True to assume no object header is present in the lexer.
        /// False to interpret the first tokens as the declaration of the object.</param>
        private void InternalParse(PdfLexer lexer, bool ignoreObjectHeader)
        {
            System.Diagnostics.Debug.Assert(null != lexer);

            // Set up the lexer
            lexer.Mode = PdfLexerMode.Pdf;

            // Retrieve the object's identifier.
            if(false==ignoreObjectHeader)
                _identifier = new PdfObjectReference(
                    ConversionUtilities.ReadInt32(lexer.ReadNextToken()),
                    ConversionUtilities.ReadInt32(lexer.ReadNextToken()));

            // We are now ready to parse the object. We expect the next token to be
            // the start of the object declaration ('obj')
            if (true==ignoreObjectHeader || "obj" == lexer.ReadNextToken())
            {
                // We now read the object tokens.
                PdfObject obj = this;
                PdfDictionary dict = null;
                PdfList array = null;
                string dictKey = null;
                bool finished = false;
                ParserReadState state = ParserReadState.Object;
                Queue<string> numbers = new Queue<string>(2);
                Stack<ParserReadState> stateStack = new Stack<ParserReadState>();
                Stack<object> parents = new Stack<object>();

                while (false == finished && lexer.Read())
                {
                    switch (state)
                    {
                        case ParserReadState.Object:
                            // If we're in the 'object' state then we treat some tokens with special care:
                            // << - starts a dictionary (changes the state)
                            // 'endobj' - ends the object
                            // 'stream' - starts a stream
                            // R - is directly added
                            // number - add the value
                            // [ - move to the Array state
                            // string - add the string
                            // hexString - add the string
                            switch (lexer.TokenType)
                            {
                                // If we're starting an array then we switch to the array state. We push the current state onto the stack.
                                case PdfTokenType.ArrayStart:
                                    stateStack.Push(state);
                                    parents.Push(obj);
                                    state = ParserReadState.Array;
                                    array = new PdfList();
                                    obj.Value = array;
                                    break;

                                // An end array token is invalid in this state (object)
                                case PdfTokenType.ArrayEnd:
                                    throw new PdfParserException("Error in PDF document - unexpected ] token in object");

                                // If we read a dictionary start token we push the current state onto the stack, and switch to dictionary state.
                                case PdfTokenType.DictionaryStart:
                                    stateStack.Push(state);
                                    parents.Push(obj);
                                    state = ParserReadState.Dictionary;
                                    dict = obj.Dictionary;
                                    break;

                                // A dictionary end token is invalid in this state (object)
                                case PdfTokenType.DictionaryEnd:
                                    throw new PdfParserException("Error in PDF document - unexpected >> token in object");

                                // If the next token is a generic token (string) we look at its name.
                                case PdfTokenType.Token:
                                    string tokenName = lexer.Value;
                                    switch (tokenName)
                                    {
                                        // The 'endobj' token ends the current object
                                        case "endobj":
                                            finished = true;
                                            break;

                                        // The 'stream' token starts a stream.
                                        // We raise an event so that listeners can handle the stream.
                                        case "stream":
                                            OnStream();
                                            // TODO: handle streams
                                            finished = true;
                                            break;

                                        // Any other token can be added to the object as it is.
                                        default:
                                            obj.SetValue(tokenName);
                                            break;
                                    }
                                    break;

                                case PdfTokenType.HexString:
                                case PdfTokenType.Name:
                                case PdfTokenType.Number:
                                case PdfTokenType.R:
                                case PdfTokenType.String:
                                    // strings and other types are directly added.
                                    obj.SetValue(lexer.Value);
                                    break;
                            }
                            break;

                        case ParserReadState.Dictionary:
                            // In the dictionary state, we treat the following tokens and names as special:
                            // << - up another dictionary entry
                            // >> - down one entry
                            // R - this could be an object reference
                            // number - remember the number in case it is part of an object reference
                            if (null == dictKey)
                            {
                                switch (lexer.TokenType)
                                {
                                    // If this token is a dictionary end token then we pop the previous state from the stack and
                                    // restore the object that we were working on before.
                                    case PdfTokenType.DictionaryEnd:
                                        state = stateStack.Pop();
                                        switch (state)
                                        {
                                            case ParserReadState.Array:
                                                array = (PdfList)parents.Pop();
                                                break;

                                            case ParserReadState.Dictionary:
                                                dict = (PdfDictionary)parents.Pop();
                                                break;

                                            case ParserReadState.Object:
                                                // If an object dictionary has been fully read then we mark
                                                // this because objects can only have one dictionary.
                                                // Also, if we are ignoring object decorations then we
                                                // stop here.
                                                parents.Pop();
                                                if (true == ignoreObjectHeader)
                                                    finished = true;
                                                break;
                                        }
                                        break;

                                    // If this token is a name then we remember it.
                                    case PdfTokenType.Name:
                                        dictKey = lexer.Value;
                                        break;

                                    case PdfTokenType.Whitespace:
                                        break;

                                    default:
                                        throw new PdfParserException(
                                            "Error in PDF document - dictionary key is not a name");
                                }
                            }
                            else
                            {
                                switch (lexer.TokenType)
                                {
                                    case PdfTokenType.ArrayEnd:
                                        throw new PdfParserException("Error in PDF document - unexpected ] token in dictionary");

                                    case PdfTokenType.ArrayStart:
                                        if (numbers.Count > 0)
                                            throw new PdfParserException("Error in PDF document - dictionary key is not a name");

                                        stateStack.Push(state);
                                        parents.Push(dict);

                                        array = new PdfList();
                                        dict.Add(dictKey, array);
                                        dictKey = null;
                                        state = ParserReadState.Array;
                                        break;

                                    case PdfTokenType.Name:
                                        // If there is a number in the queue, we accept that as the dictionary value.
                                        // We then open a new dictionary key for the new name.
                                        // If there are no numbers in the queue, we use the name as the dictionary value.
                                        if (numbers.Count > 0)
                                        {
                                            dict.Add(dictKey, numbers.Dequeue());

                                            if (numbers.Count > 0)
                                                throw new PdfParserException(
                                                    "Error in PDF document - dictionary key is not a name");

                                            dictKey = lexer.Value;
                                        }
                                        else
                                        {
                                            dict.Add(dictKey, lexer.Value);
                                            dictKey = null;
                                        }

                                        break;

                                    case PdfTokenType.Number:
                                        // If there is still space for another number, we stick it in. There can only
                                        // be two numbers in the queue.
                                        if (2 == numbers.Count)
                                            throw new PdfParserException(
                                                "Error in PDF document - dictionary key is not a name");

                                        numbers.Enqueue(lexer.Value);
                                        break;

                                    case PdfTokenType.R:
                                        // If we have two numbers, we will have found an indirect reference.
                                        // Any other state will be invalid.
                                        if (2 == numbers.Count)
                                        {
                                            
                                            dict.Add(dictKey,
                                                new PdfObjectReference(
                                                    ConversionUtilities.ReadInt32(numbers.Dequeue()),
                                                    ConversionUtilities.ReadInt32(numbers.Dequeue())));

                                            dictKey = null;
                                        }
                                        else
                                            throw new PdfParserException(
                                                "Error in PDF document - dictionary key is not a name");

                                        break;

                                    case PdfTokenType.DictionaryStart:
                                        if (numbers.Count > 0)
                                            throw new PdfParserException("Error in PDF document - dictionary key is not a name");

                                        stateStack.Push(state);
                                        parents.Push(dict);

                                        PdfDictionary newDict = new PdfDictionary();
                                        dict.Add(dictKey, newDict);
                                        dict = newDict;
                                        dictKey = null;
                                        break;

                                    case PdfTokenType.DictionaryEnd:
                                        // If there is a number in the queue, we accept that as the dictionary value.
                                        if (numbers.Count > 0)
                                            dict.Add(dictKey, numbers.Dequeue());
                                        else
                                            dict.Add(dictKey, String.Empty);

                                        state = stateStack.Pop();
                                        switch (state)
                                        {
                                            case ParserReadState.Array:
                                                array = (PdfList)parents.Pop();
                                                break;

                                            case ParserReadState.Dictionary:
                                                dict = (PdfDictionary)parents.Pop();
                                                dictKey = null;
                                                break;

                                            case ParserReadState.Object:
                                                // If an object dictionary has been fully read then we mark
                                                // this because objects can only have one dictionary.
                                                // Also, if we are ignoring object decorations then we
                                                // stop here.
                                                parents.Pop();
                                                if(true==ignoreObjectHeader)
                                                    finished = true;
                                                break;
                                        }
                                        break;

                                    case PdfTokenType.HexString:
                                    case PdfTokenType.String:
                                    case PdfTokenType.Token:
                                        // Anything else gets written to the destination stream.
                                        if (numbers.Count > 0)
                                            throw new PdfParserException("Error in PDF document - dictionary key is not a name");

                                        dict.Add(dictKey, lexer.Value);
                                        dictKey = null;
                                        break;
                                }
                            }
                            break;

                        case ParserReadState.Array:
                            // In the array state, we treat the following tokens and names as special:
                            // << - up another dictionary entry
                            // >> - down one entry
                            // R - this could be an object reference
                            // number - remember the number in case it is part of an object reference
                            switch (lexer.TokenType)
                            {
                                case PdfTokenType.ArrayEnd:
                                    while (numbers.Count > 0) array.Add(numbers.Dequeue());

                                    state = stateStack.Pop();
                                    switch (state)
                                    {
                                        case ParserReadState.Array:
                                            array = (PdfList)parents.Pop();
                                            break;

                                        case ParserReadState.Dictionary:
                                            dict = (PdfDictionary)parents.Pop();
                                            break;

                                        case ParserReadState.Object:
                                            // If an object array has been fully read then we mark
                                            // this because objects can only have one dictionary or array.
                                            // Also, if we are ignoring object decorations then we
                                            // stop here.
                                            parents.Pop();
                                            if (true == ignoreObjectHeader)
                                                finished = true;
                                            break;
                                    }
                                    break;

                                case PdfTokenType.ArrayStart:
                                    while (numbers.Count > 0) array.Add(numbers.Dequeue());

                                    stateStack.Push(state);
                                    parents.Push(array);

                                    PdfList newArray = new PdfList();
                                    array.Add(newArray);
                                    array = newArray;
                                    break;

                                case PdfTokenType.Number:
                                    // If we have two numbers, we dequeue one. We stick in the new number.
                                    if (2 == numbers.Count) array.Add(numbers.Dequeue());
                                    numbers.Enqueue(lexer.Value);
                                    break;

                                case PdfTokenType.R:
                                    // If we have two numbers, we will have found an indirect reference.
                                    // Otherwise, we output the remaining number and the R
                                    if (2 == numbers.Count)
                                    {
                                        array.Add(
                                            new PdfObjectReference(
                                                ConversionUtilities.ReadInt32(numbers.Dequeue()),
                                                ConversionUtilities.ReadInt32(numbers.Dequeue())));

                                        dictKey = null;
                                    }
                                    else
                                    {
                                        array.Add(numbers.Dequeue());
                                        array.Add("R");
                                    }
                                    break;

                                case PdfTokenType.DictionaryStart:
                                    while (numbers.Count > 0) array.Add(numbers.Dequeue());

                                    stateStack.Push(state);
                                    parents.Push(array);

                                    dict = new PdfDictionary();
                                    array.Add(dict);
                                    dictKey = null;
                                    state = ParserReadState.Dictionary;
                                    break;

                                case PdfTokenType.DictionaryEnd:
                                case PdfTokenType.HexString:
                                case PdfTokenType.Name:
                                case PdfTokenType.String:
                                case PdfTokenType.Token:
                                    // Anything else is read in straightaway.
                                    while (numbers.Count > 0) array.Add(numbers.Dequeue());
                                    array.Add(lexer.Value);
                                    break;
                            }
                            break;

                        case ParserReadState.Stream:
                            // In the stream state we are parsing a plain-text stream.
                            // We do not treat any token as special except for 'endstream' which ends the
                            // stream.
                            if (PdfTokenType.Token == lexer.TokenType)
                            {
                                int index, finger = _endStream.Length - 1;
                                for (index = lexer.TokenLength - 1; index >= Math.Max(0, lexer.TokenLength - _endStream.Length); index--)
                                    if (lexer.Buffer[lexer.TokenStart + index] != _endStream[finger--]) break;

                                if (finger < 0)
                                {
                                    state = ParserReadState.Object;
                                    finished = true;
                                }
                                else
                                {
                                    // _out.Write(_lexer.Buffer, _lexer.TokenStart, _lexer.TokenLength);
                                }
                            }
                            break;
                    } // switch state

                } // while there is more to read..

            } // If this is a valid object declaration
            else
                throw new PdfParserException(String.Format("Expected token 'obj', found '{0}' instead", lexer.Value));

        } // InternalParse()
        #endregion



        // Public constructor
        // ==================
        #region Public constructor
        /// <summary>
        /// Constructs a new PdfObject instance.
        /// </summary>
        public PdfObject()
        {
            _identifier = PdfObjectReference.Empty;
            _dictionary = new PdfDictionary();
            _value = _dictionary;
        }
        #endregion



        // Private types
        // =============
        #region Private types
        /// <summary>
        /// The ParserReadState enumeration lists the possible states that the parser can be in while reading
        /// bytes off the stream.
        /// </summary>
        public enum ParserReadState
        {
            Array,
            Dictionary,
            Object,
            Stream
        }
        #endregion

    } // PdfObject class
}
